Skip to content

Engagement scope guard: every network target read and checked (DNS, scanners, nc) - #18

Merged
mkhellat merged 5 commits into
mainfrom
feat/scope-block
Nov 12, 2026
Merged

mkhellat merged 5 commits into
mainfrom
feat/scope-block

Conversation

@mkhellat

Copy link
Copy Markdown
Owner

PR 4 of the phase 10, step 5 plan (decision D14i): the engagement scope guard. Until now no code read a network step's target; the engagement's scope was only recorded in the evidence log.

Commits

  • 3dcff8d feat(safety): let an optional layer register step checks. The core keeps add_step_check() and add_step_notice() hook lists, which SafetyEngine asks after its own rules, for every step and again right before it runs. A hook that raises refuses the step. The core never imports the security layer.

  • 35841ce feat(security): the engagement scope guard (DNS, scanners, nc).

    • Active tools (nmap, masscan, nc, ncat): refused out of scope, on excluded targets, or outside the window. A hard block, given the AISI 2026 evidence that prompt-level scope isn't enough.
    • Passive lookups (dig, host): ask for confirmation instead.
    • Unreadable targets are refused: unknown options, since their values could be targets, and nmap -iR, --resume, masscan -c, dig -f.
    • Third-party hosts are targets too: DNS servers, proxies, nmap's idle-scan zombie and FTP relay.
    • Ranges must fit in the scope whole, excluded networks included. A name never matches addresses.
    • Also refused: a network step with a tool the guard can't read, and an engagement at trusted.

    CORE_API widens by codebud.safety and codebud.privacy, deliberately.

The guard isn't reachable by users yet: the codebud-sec entry point (PR 7) opens the engagement and activates it. HTTP tools, web fuzzers and TLS scanners follow in PR 4b.

Testing

1516 tests pass (63 new). Ruff is clean, the manual builds without warnings, and make distcheck passes.

Phase 10, step 5 plan, PR 4.  The engagement scope check belongs to the
security layer, but steps are judged in the core, which never imports
that layer.  The core now keeps two hook lists:

  - add_step_check(fn): fn(step) returns why the step is refused, or
    None;
  - add_step_notice(fn): fn(step) returns why it must be confirmed, or
    None.

SafetyEngine asks the checks after its own rules (an allow-list refusal
is reported as such first), for every step and again right before it
runs (may_execute), and the notices in confirmation_reason.  A hook
that raises refuses the step: a broken check must never let one
through.  The engine does not know what the hooks are -- complete
mediation without a dependency on the layer.  Manual (Safety and
Sandbox > The Safety Engine) and the test list updated.
Phase 10, step 5 plan, PR 4 (decision D14i).  Until now no code read a
network step's target: the engagement's scope was only recorded.
codebud_sec/scope.py reads every target a network tool will contact
from its command line and checks it against the engagement:

  - active tools (nmap, masscan, nc, ncat) are refused when a target is
    outside the scope, excluded, or the window is closed -- a hard
    block: a model's plan is untrusted, and the UK AISI incident report
    (4 Aug 2026) shows prompt-level scope is not enough;
  - passive lookups (dig, host) out of scope ask for confirmation,
    D14's warn-and-confirm kept for them.

A target that cannot be read is refused.  Each tool's parser knows which
options take values; an unknown option is refused, since its value
could be a target the guard cannot see.  Hosts a tool contacts besides
its target are targets too: DNS servers (dig @, nmap --dns-servers),
proxies (nc -x, ncat --proxy, nmap --proxies), the idle-scan zombie
(nmap -sI) and the FTP relay (nmap -b).  nmap -iR (random hosts),
--resume, masscan -c and dig -f are refused; target lists (-iL) are
read only from a file inside the project.  Ranges -- CIDRs, masscan
a-b, nmap octet ranges by their bounding range -- must fit in the scope
as a whole, and engagement.covers_network() checks that against the
excluded networks too.  A name never matches addresses: resolving it
would itself be a network action.

A step that declares the network with a tool the guard cannot read is
refused, and an engagement at privacy trusted (no isolation) refuses
every command.  ScopeGuard.activate() registers the guard with the
core's step-check hook; the codebud-sec entry point will open the
engagement and activate it (PR 7).  CORE_API widens by codebud.safety
(the hook) and codebud.privacy, deliberately.

HTTP tools, web fuzzers and TLS scanners follow in PR 4b.  Architecture
layout, build list and test list updated.
@mkhellat

Copy link
Copy Markdown
Owner Author

Review summary: changes requested

One full sweep over 35841ce: conditions → scenarios → each checked against the code → De Morgan.

Conditions.

  • A step is refused ⇔ privacy = trusted ∨ unreadable(targets) ∨ (no parser ∧ network declared) ∨ ∃ active tool with a target ∉ scope ∨ (active ∧ window closed).
  • It's asked ⇔ a passive tool has a target ∉ scope.
  • Targets are read for every word naming a tool, in every command step.

Correct, verified (63 tests):

  • Per-tool target reading, third-party hosts, CIDR/range/octet-range bounding, covers_network with exclusions.
  • Lists only from project files; unknown options refused.
  • Hard block for active tools, warning for passive ones.
  • Closed window, privacy trusted and unparsed network tools refused.
  • Found behind wrappers and under variant names; deactivate works.

Defects (reproduced on 35841ce):

  1. False refusals of data. grep host /etc/hosts and grep -c nmap auth.log are REFUSED: the word is read as the tool, and its "targets" fail. "host" is a common word in log triage. A step that doesn't declare the network can't reach anything when isolated, so only network steps need a scope check.

  2. That relies on isolation, which the guard doesn't require. At private with no isolation tool, codebud warns and runs steps unisolated, so an undeclared step has the network. Fix: an engagement refuses every command step unless isolation is available. This widens CORE_API by codebud.isolation.

  3. nmap scans past its command line (ALLOWED today):

    • --script broadcast-* (and the broadcast category) sends to the whole local network.
    • targets-* scripts with --script-args newtargets add the hosts they discover as scan targets.

    Fix: refuse those script names, the broadcast category, and newtargets.

Verdict: changes requested. The fixes come as new commits, then a re-review.

Review of PR #18, findings 1 and 2.  The guard read targets from every
word naming a tool, in every command step, so data was taken for a
command: `grep host /etc/hosts` and `grep -c nmap auth.log` were
refused, "host" and "nmap" read as the tools.  A step that does not
declare the network contacts no one when it is isolated, so only network
steps are checked now.

That relies on isolation, which the guard did not require: at privacy
private with no isolation tool, codebud warns and runs steps
unisolated, and an undeclared step then has the network.  An engagement
now refuses every command step unless isolation is available (and the
privacy level is not trusted).  CORE_API widens by codebud.isolation,
deliberately.

The tests now decide isolation for themselves instead of inheriting
the machine's (macOS CI has no bubblewrap); the suite was also run with
isolation forced unavailable.
Review of PR #18, finding 3.  nmap's scripts can contact hosts the
command line never names, and the guard let them through:

  - broadcast-* scripts send to the whole local network; they are in
    the broadcast and discovery categories and partly in safe, so
    --script safe or --script discovery runs them too;
  - targets-* scripts with --script-args newtargets add the hosts they
    find as scan targets;
  - external scripts send data to third-party services (whois, ASN).

--script is refused when any name, category or glob in it could select
these -- broadcast, discovery, safe, external, all, broadcast*,
targets-*, or a glob matching them -- in a list or a boolean expression
(it errs strict: "not broadcast" is refused too, since expressions are
not evaluated); --script-args with newtargets is refused.  -sC and
named scripts such as http-title stay allowed.  Eleven cases, all
allowed before.
Re-review of PR #18.  The guard judged only command steps, but
web_search is a network step too: it sends the query to DuckDuckGo, a
third party outside any engagement.  Web search is off by default and
the security profiles keep it off, but the guard should not depend on a
setting; it now refuses web_search steps.  File tools and host_network
stay on this machine and are untouched (tested).
@mkhellat

Copy link
Copy Markdown
Owner Author

Re-review summary: fixed, ready to merge once CI passes

Second sweep, over 098f3f8.

Finding Fix Verified
1. Data taken for a tool (grep host /etc/hosts refused) 57c4e70: only steps that declare the network are checked 3 cases refused before, allowed now
2. Network-only checking needs isolation, which wasn't required 57c4e70: an engagement refuses every command step unless isolation is available (and privacy isn't trusted); CORE_API += codebud.isolation test without isolation. The suite was also run with isolation forced unavailable, as on macOS CI, and the fixtures now set isolation themselves.
3. nmap scripts reaching past the command line f1fe0fd: --script naming broadcast/discovery/safe/external/all, broadcast*, targets-* or a matching glob, in lists or expressions, is refused, as is --script-args newtargets; -sC and named scripts stay allowed 11 cases allowed before, refused now
4. (this sweep) web_search not judged: it sends to DuckDuckGo 098f3f8: refused under an engagement; file tools and host_network untouched fails before, passes now

Erring strict, documented in the code: --script 'not broadcast' is refused, because expressions aren't evaluated.

1535 tests pass, ruff is clean, and make distcheck passes. Verdict: correct. I'll merge with a merge commit once CI passes on all jobs, macOS included.

@mkhellat

Copy link
Copy Markdown
Owner Author

Merging. All four review findings are fixed and re-verified: data words, isolation, nmap scripts, web_search. CI is green on all five jobs at 098f3f8. The one failure on the first run (macOS Py 3.10, test_crypto) was age refusing a correct passphrase on a slow moment. #19 fixes that, now on main, and the re-run passed. Since main moved (#19), I tested this branch merged onto it locally: it merges cleanly, 1539 tests pass (the re-review's "1535" was a miscount; this branch alone has 1536), and make distcheck passes. A merge commit keeps the five commits as written.

Next, from the step 5 plan: PR 4b, target parsers for the HTTP tools, web fuzzers and TLS scanners.

@mkhellat
mkhellat merged commit 5adb1c1 into main Nov 12, 2026
9 of 10 checks passed
@mkhellat
mkhellat deleted the feat/scope-block branch November 12, 2026 08:59
mkhellat added a commit to Ixzu-12/codebud that referenced this pull request Nov 12, 2026
Found on macOS CI (PR mkhellat#18's run, macOS Py 3.10): unlocking a key made a
moment earlier failed with pyrage's "Excessive work parameter for
passphrase", which codebud reported as "wrong passphrase".  age sets
the passphrase's scrypt work factor from the machine's speed when it
seals the key, and on every unlock measures the speed again and refuses
a work factor far above it.  On a busy or throttled moment -- a loaded
runner, a laptop on battery -- a correct passphrase is refused, and
codebud told the user it was wrong: a lockout that sends them looking
for a typo, or giving their data up for lost.

pyrage offers no control over the work factor, so codebud now retries
that specific refusal (age measures again each time) three times, then
says what happened -- the machine was slower than when the key was
sealed, the passphrase may well be right -- instead of "wrong
passphrase".  A genuinely wrong passphrase is still reported as such,
and not retried.  The manual's Troubleshooting chapter gains "Key
Refused on a Slow Moment", including the persistent case of a key
moved to a much slower machine.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant